<?php
/**
 * Class that provide information about registered plugins and has methods for plugin registration process.
 * 
 * LICENSE:
 * 
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with This library. If not, see <{@link http://www.gnu.org/licenses/}>.
 *
 * @link http://code.google.com/p/phpstartapp/
 * @author Bojan Mauser <bmauser@gmail.com>
 * @copyright Bojan Mauser 2009
 * @package psa
 * @version $Id: Psa_Plugin.class.php 464 2009-03-24 00:30:12Z bmauser $
 */


/**
 * Class that provides information about registered plugins and has methods for plugin registration process.
 * Information about plugins must be firstly stored (registered)in the database 
 * with {@link register()} method or by calling {@link register_plugins.php} script.
 * Plugin registration is storing information about plugins in the database, 
 * and this should be done when new plugin class is written. If {@tutorial psa_features.pkg#developmode develop mode} is on, 
 * registration is done on every request for convenience that you don't need manually 
 * register if you add some plugin class.
 * Read more about plugins {@tutorial psa_features.pkg#plugins here}.
 * 
 * This class implements {@link http://en.wikipedia.org/wiki/Singleton_pattern singleton pattern}
 * so you can get reference to result object from any scope with 
 * {@link get_instance()} method. You cannot make instance of Psa_Plugin object with 
 * <var>new</var> operator.
 * 
 * Information about plugins must be available on every request. So, to not query the database
 * on every request you can store this data into session. See {@link session_restore()} method.
 * 
 * @see register()
 * @see register_plugins.php
 */
class Psa_Plugin extends Psa_Singleton{

	/**
	 * Array with data about registered plugins.
	 * This array is set with {@link set_data()} method.
	 * Array structure is the same as array returned from {@link register()} method
	 * but without <kbd>error</kbd> element. 
	 * 
	 * @var array
	 * @see register()
	 */
	public $plugins_data = array();	
	
	
	/**
	 * Flag if object is serialized to session with {@link session_save()} method.
	 * 
	 * @see session_save()
	 * @var string 
	 * @ignore 
	 */
	protected $saved_to_session = null;
	
	
	/**
	 * Returns object's instance.
	 * You should statically call this method with scope resolution operator (::) which gives you
	 * instance to the object from any scope in your application.
	 * Example:
	 * <code>
	 * <?php
	 * 
	 * $plugins = Psa_Plugin::get_instance();
	 * 
	 * ?>
	 * </code>
	 */
	public static function get_instance($classname = __CLASS__){
		return parent::get_instance($classname);
	}
	
	
	/**
	 * Reads data about registered plugins from database and fills {@link $plugins_data} member array.
	 * 
	 * <b>Note:</b> you don't need to call this method if you called {@link save()} or {@link register()}
	 * method before cause {@link register()} method will also fill {@link $plugins_data} array.
	 * This method only reads previously saved plugins data from database.
	 * 
	 * @see $plugins_data
	 * @see register()
	 * @see save()
	 * @return int 1-sucess, 0-failure
	 */
	public function set_data() {
	
		// global config array
		global $PSA_CFG;
		
		// global database object
		global $psa_database;
	
		// sql query
		$sql = "SELECT * FROM {$PSA_CFG['database']['table']['plugin']} WHERE id = '1'";
		
		// run query against the database
		$psa_database->query($sql);

		// get results
		$result = $psa_database->fetch_row();
		
		if($result['plugins_data']){
			$this->plugins_data = unserialize($result['plugins_data']);
			return 1;
		}
		else
			return 0;
	}
	
	
	/**
	 * Stores object into PHP session or returns serialized representation of the Psa_Plugin object.
	 * This is useful if you want to store complete object and to restore it on next request without
	 * need to query the database with {@link set_data()} method.
	 * Also, if you store object into session with this method, session variable that holds 
	 * serialized data will be updated on every call to {@link save()} method.
	 * The name of session variable will be <kbd>psa_plugins</kbd>.
	 * PHP sessions have to be started with <kbd>session_start()</kbd> function before calling this method
	 * otherwise throws {@link Psa_Plugin_Exception}.
	 * 
	 * @param bool $only_return if true, only serialized string will be returned.
	 * @return int|string
	 * @see session_restore()
	 */
	public function session_save(){
		
		// check if session is started
		if(!session_id()){
			include_once 'Psa_Plugin_Exception.class.php';
			throw new Psa_Plugin_Exception('Session is not started. Cannot store Psa_Plugin object into session.', 51);
		}
		
		$this->saved_to_session = 1;
		
		$_SESSION['psa_plugins'] = $this;
	}
	
	
	/**
	 * Restores object from session previously saved {@link session_save()} method.
	 * If there is no stored object in session it calls {@link set_data()} method and then 
	 * {@link session_save()} method so you actually need to call only this method if you want
	 * to put {@link Psa_Plugin} object into session.
	 * If {@tutorial psa_features.pkg#developmode develop mode} is on, data about plugins are always
	 * read from database.
	 * 
	 * <b>Example:</b>
	 * 
	 * <code>
	 * <?php
	 * 
	 * // Router plugin.
	 * class Router extends Psa_Plugin_Router{
	 * 
	 * 	function psa_main(){
	 * 		
	 * 		// start session
	 * 		session_start();
	 * 		
	 * 		// restore plugin object from session
	 * 		Psa_Plugin::get_instance()->session_restore();
	 * 		
	 * 		// some router logic here
	 * 	}
	 * }
	 * 
	 * ?>
	 * </code>
	 * 
	 * @param string $serialized_data return from {@link session_save()}
	 * @return int
	 * @see session_save()
	 */
	public function session_restore(){
		
		global $PSA_CFG;
		
		// restore object from session
		if(!$PSA_CFG['develop_mode'] && @$_SESSION['psa_plugins']){
			$serialized_object = $_SESSION['psa_plugins'];
		
			// set current objects properties
			if(is_object($serialized_object)){
				foreach ($serialized_object as $serialized_object_key => $serialized_object_value) {
					$this->$serialized_object_key = $serialized_object_value;
				}
			}
		}
		// restore object from database and store it into session
		else{
			if($this->set_data())
				$this->session_save();
			else return 0;
		}
		
		return 1;
	}
	
	
	/**
	 * Registers plugins.
	 * It searches for available plugin type classes (class names start with <i>Psa_Plugin_</i>) 
	 * in folders defined in <kbd>$PSA_CFG['folders']['plugin_types']</kbd> config array. The default is
	 * <kbd>plugin_classes</kbd> folder. Then searches for classes that extends those
	 * plugin type classes in all .php files in folders specified in <kbd>$PSA_CFG['folders']['plugins']</kbd>
	 * config array (defined in {@link config.php} file) and returns array with all found plugin 
	 * files and errors. It also fills {@link $plugins_data} array with the same data.
	 * Files that contain text 'PSA_SKIP_REGISTER' anywhere in content will be skipped from 
	 * registration and plugins in those files won't be registered.
	 * <br>Returning array may look like this:<code>
	 * Array
	 * (
	 *     [Psa_Plugin_After_User_Create] => Array
	 *         (
	 *             [My_Plugin] => Array
	 *                 (
	 *                     [file] => /var/www/psa/plugins/My_Plugin.php
	 *                 )
	 * 
	 *         )
	 * 
	 *     [Psa_Plugin_Controller] => Array
	 *         (
	 *             [Default_Controller] => Array
	 *                 (
	 *                     [file] => /var/www/psa/plugins/default_controller.php
	 *                 )
	 *         )
	 * 
	 *     [all_plugins] => Array
	 *         (
	 *             [My_Plugin] => Array
	 *                 (
	 *                     [file] => /var/www/psa/plugins/My_Plugin.php
	 *                     [type] => Psa_Plugin_After_User_Create
	 *                 )
	 * 
	 *             [Default_Controller] => Array
	 *                 (
	 *                     [file] => /var/www/psa/plugins/default_controller.php
	 *                     [type] => Psa_Plugin_Controller
	 *                 )
	 * 
	 *         )
	 * 
	 *     [error] => Array
	 *         (
	 *             [already_registered_class_name] => Array
	 *                 (
	 *                     [2] => Array
	 *                         (
	 *                             [plugin] => Psa_Plugin_After_User_Delete
	 *                             [file] => /var/www/psa/plugins/My_Plugin2.php
	 *                             [class] => My_Plugin
	 *                             [conflicted_with] => /var/www/psa/plugins/My_Plugin.php
	 *                         )
	 * 
	 *                     [4] => Array
	 *                         (
	 *                             [plugin] => Psa_Plugin_After_User_Create
	 *                             [file] => /var/www/psa/plugins/My_Plugin3.php
	 *                             [class] => My_Plugin
	 *                             [conflicted_with] => /var/www/psa/plugins/My_Plugin.php
	 *                         )
	 *                 )
	 *         )
	 * 
	 *     [all_psa_plugin_types] => Array
	 *         (
	 *             [Psa_Plugin_Controller] => /myapp/psa/plugin_classes/Psa_Plugin_Controller.class.php
 	 *             [Psa_Plugin_Model] => /myapp/psa/plugin_classes/Psa_Plugin_Model.class.php
 	 *             [Psa_Plugin_Router] => /myapp/psa/plugin_classes/Psa_Plugin_Router.class.php
 	 *             [Psa_Plugin_View] => /myapp/psa/plugin_classes/Psa_Plugin_View.class.php
	 *              ...
	 *          )
	 * )
	 * </code>
	 * 
	 * @return array|int array with plugin data or 0 for failure
	 * @see save()
	 * @see config.php
	 */
	function register(){
	
		global $PSA_CFG;
		
		// find all available plugins types from file names in plugin dir: plugin_classes
		foreach ($PSA_CFG['folders']['plugin_types'] as $plugin_type_folder) {
			
			$plugin_def_dir = $PSA_CFG['folders']['basedir'] . '/' . $plugin_type_folder;
			
			if ($handle = @opendir($plugin_def_dir)) {
				while (false !== ($file = readdir($handle))) {
		
					if (substr($file, 0, 11) == 'Psa_Plugin_'){
						// get the part of the filename to the first dot. This is the name of the plugin class.
						$psa_plugin_class_name = str_replace(strstr($file, '.'), '', $file);
						$all_psa_plugin_types[$psa_plugin_class_name] = $plugin_def_dir . '/' . $file;
					}
				}
				closedir($handle);
			}
			else{
				include_once 'Psa_Plugin_Exception.class.php';
				throw new Psa_Plugin_Exception("plugin register: unable to open dir with plugin type classes: $plugin_def_dir", 53);
			}
		}
		
		// if some plugins are found 
		if(@$all_psa_plugin_types){
			
			$return = array();

			// search for plugins inside folders specified in $PSA_CFG['folders']['plugins'] array
			if(is_array($PSA_CFG['folders']['plugins'])){
				foreach ($PSA_CFG['folders']['plugins'] as $plugins_folders_key => $plugins_folders_value){
				
					// open each .php file and find classes which extends plugin classes
					$this->recursive_check_plugins($PSA_CFG['folders']['basedir'] . "/$plugins_folders_value", $all_psa_plugin_types, $return);
				}
			}
			
			// if Router.php location is set in config file register it
			if(isset($PSA_CFG['router_location'])){
				$router_filepath = $PSA_CFG['folders']['basedir'] . '/' . $PSA_CFG['router_location'] . '/' . 'Router.php';
				$return['Psa_Plugin_Router']['Router']['file'] = $router_filepath;
				$return['all_plugins']['Router']['file'] = $router_filepath;
				$return['all_plugins']['Router']['type'] = 'Psa_Plugin_Router';
			}
			
			// put also PSA plugins types into return array which will be used for class autoloading
			$return['all_psa_plugin_types'] = $all_psa_plugin_types;
			
			return $this->plugins_data = $return;
		}
		else{
			// if logging is enabled
			if($PSA_CFG['logging']['max_log_level'] >= 2){
				// parameters for Psa_Logger::log() method
				$log_data['message']  = "plugin register: no plugins found for registration";
				$log_data['function'] = __FUNCTION__;
				$log_data['level']    = 2;
				Psa_Logger::get_instance()->log($log_data);
			}
			
			return 0;
		}
	}
	
	
	/**
	 * Recursively checks for plugins inside <kbd>$dir</kbd>.
	 * This is helper method for register().
	 * 
	 * @see register()
	 * @return int 1-sucess, 0-if cannot open <kbd>$dir</kbd> or array with plugin data
	 * @ignore 
	 */
	function recursive_check_plugins($dir, $all_psa_plugin_types, &$return, $recursion_depth = 0){
		
		static $plugins = array();
		static $plugins_index = 0;
		global $PSA_CFG;
		
		
		if(!file_exists($dir) or !$handle = opendir($dir)){
			
			// if logging is enabled
			if($PSA_CFG['logging']['max_log_level'] >= 2){
				// parameters for Psa_Logger::log() method
				$log_data['message']  = "folder with plugins $dir don't exists or not readable";
				$log_data['function'] = __FUNCTION__;
				$log_data['level']    = 2;
				Psa_Logger::get_instance()->log($log_data);
			}
			
			return 0;
		}
		
		
		if ($handle){
			
			while (($file = readdir($handle)) !== false){
				
				if (($file == ".") || ($file == ".." ) || (strcasecmp($file,'.svn') == 0) || (strcasecmp($file,'.cvs') == 0)){
					continue;
				}
				
				// full filesystem path
				$filepath = $dir . '/' . $file;
				
				if (is_dir($filepath)){
					// call self for this directory
					$this->recursive_check_plugins($filepath, $all_psa_plugin_types, $return, $recursion_depth+1);
				}
				// check for plugins
				else{
					// if file extension is '.php'
					if(substr($file, -4, 4) == '.php'){
					
						// Entire file content.
						// I guess you won't have large .php files to consume all memory limited by memory_limit php.ini directive
						$file_content = file_get_contents($filepath);
						
						// skip files that contains 'PSA_SKIP_REGISTER' text anywhere
						if(!(strpos($file_content,'PSA_SKIP_REGISTER') === false))
							continue;
						
						// find classes that extends plugin classes
						foreach ($all_psa_plugin_types as $psa_plugin_type => $psa_plugin_type_path){
							
							// match class name that extends plugin class
							if(preg_match('/class +([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*).*? extends +' . $psa_plugin_type . '/',$file_content,$matches)){
								
								$plugins_index ++;
								
								$skip_plugin = 0;
								
								// plugin class name
								$class_name = $matches[1];
								
								// check if plugin is already registered. Search for duplicate class names within already registered plugins 
								if(isset($plugins['all_plugins']['$class_name'])){
									
									$plugins['error']['already_registered_class_name'][$plugins_index]['plugin'] = $psa_plugin_type;
									$plugins['error']['already_registered_class_name'][$plugins_index]['file'] = $filepath;
									$plugins['error']['already_registered_class_name'][$plugins_index]['class'] = $class_name;
									$plugins['error']['already_registered_class_name'][$plugins_index]['conflicted_with'] = $plugins_value['file'];
						
									$skip_plugin = 1;
									$log_data['message']  = "plugin register: skipping plugin. {$filepath} uses same class name {$class_name} as previously registered {$plugins_value['file']}";
								}
								
								// check if router plugin is already registered. There can be only one router plugin registered.
								if(isset($plugins['Psa_Plugin_Router']) && $psa_plugin_type == 'Psa_Plugin_Router'){
									$plugins['error']['already_registered_router_plugin'][$plugins_index]['plugin'] = $psa_plugin_type;
									$plugins['error']['already_registered_router_plugin'][$plugins_index]['file'] = $filepath;
									$plugins['error']['already_registered_router_plugin'][$plugins_index]['class'] = $class_name;
									$plugins['error']['already_registered_router_plugin'][$plugins_index]['conflicted_with'] = $plugins['Psa_Plugin_Router']['Router']['file'];
								
									$skip_plugin = 1;
									$log_data['message']  = "plugin register: skipping plugin {$filepath}. Router plugin is already registered in {$plugins['Psa_Plugin_Router']['Router']['file']}";
								}
	
								
								
								
								if(!$skip_plugin){
									
									$plugins[$psa_plugin_type][$class_name]['file'] = $filepath;
									
									$plugins['all_plugins'][$class_name]['file'] = $filepath;
									$plugins['all_plugins'][$class_name]['type'] = $psa_plugin_type;
								}
								else{
									// write errors to log
									if($PSA_CFG['logging']['max_log_level'] >= 1){
										// parameters for Psa_Logger::log() method
										$log_data['function'] = __FUNCTION__;
										$log_data['level']    = 2;
										$log_data['type']     = 'warning';
										Psa_Logger::get_instance()->log($log_data);
									}
								}
							}
						}
					}
				}
			}
			unset($file_content);
			
			@closedir($handle);
		}
		else
			return 0;
			
		$return = $plugins;
	}
	
	
	/**
	 * Saves data about registered plugins returned from {@link register()} method to the database.
	 * If called without arguments it first calls {@link register()} method.
	 * Throws {@link Psa_Plugin_Exception} on error.
	 * 
	 * @param array $plugins_data array returned from {@link register()} method
	 * @see register()
	 * @return int 1-sucess
	 */
	function save($plugins_data = null){
	
		// call register() method if no data is passed
		if(!$plugins_data)
			$plugins_data = $this->register();
		
		// global config array
		global $PSA_CFG;
		
		// global database object
		global $psa_database;
	
		// delete errors from $plugins_data cause we do not need them in the database
		unset($plugins_data['error']);
		
		// serialize data
		$serialized_data = $psa_database->escape(serialize($plugins_data));
		
		// sql query
		$sql = "UPDATE {$PSA_CFG['database']['table']['plugin']} SET plugins_data = $serialized_data WHERE id = '1'";
	
		// run query against the database
		$psa_database->query($sql);
	
		try{
			$psa_database->query($sql);
			
			return 1;
		}
		catch (Psa_Db_Exception $e){
				
			include_once 'Psa_Plugin_Exception.class.php';
			throw new Psa_Plugin_Exception('Error saving data about registered plugins', 52);
		}
	}
	
}

